/*
 * TetrisPanel.java
 *
 * Created on Dec 15, 2008, 6:47:57 PM
 */

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.JOptionPane;

/**
 * 
 * @author Jordan Lange
 */
public class TetrisPanel extends javax.swing.JPanel implements Runnable,
		KeyListener {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private static final int INFO_PANEL_WIDTH = 200;
	private static final int SPEED_UP_RATE = 50;
	private static final int BLOCKS_PER_LEVEL = 5;
	private static final String SCORE_FILE = "Scores.dat";

	private Piece currMovingPiece;
	private TetrisGrid grid;
	private double speedFactor = 1.0;
	private boolean playable = true, paused = false, won = false,
			dropping = false, nosleep = false, movingPiece = false;
	private Tetris.Shape nextPiece = Tetris.getRandomShape();
	private int score = 0;
	private Color infoPanelColor = new Color(215, 213, 210);
	private int barsDestroyed = 0;
	private int level = 1;
	private Timer timer;
	private Scores scores;
	private ScoreFrame sf;
	private File scoreFile;

	/** Creates new form TetrisPanel */
	public TetrisPanel(int width, int height) {
		int boxSize = TetrisGrid.getBoxSize();
		grid = new TetrisGrid(((width - INFO_PANEL_WIDTH) / boxSize) - 1,
				(height / boxSize) - 2);
		scoreFile = new File(SCORE_FILE);
		scores = new Scores(scoreFile);
		sf = new ScoreFrame(scores);
		this.setFocusable(true);
		this.addKeyListener(this);
		new Thread(this).start();
	}

	public void run() {
		while (playable && !paused) {
			if (currMovingPiece == null) {
				if (level >= (int) (1000 / SPEED_UP_RATE)) {
					winGame();
					return;
				}
				// Check for a non-broken line (only check when there's no
				// moving piece)
				int res = -1, factor = 0, tempScore = 0;
				while ((res = grid.getUnbrokenRow()) != -1) {
					factor++;
					grid.removeRow(res);
					tempScore += 100 * level;
					barsDestroyed++;
				}
				// Statistics
				score += tempScore * factor;
				level = 1 + (barsDestroyed / BLOCKS_PER_LEVEL);
				speedFactor = Math.abs(1000.0 - (level * SPEED_UP_RATE));
				// add a piece
				currMovingPiece = new Piece(nextPiece, new Point(grid
						.getGridSize().width / 2, 0));
				if (grid.isValid(currMovingPiece)) {
					grid.addPiece(currMovingPiece);
					nextPiece = Tetris.getRandomShape();
				} else {
					endGame();
					return;
				}
			}
			try {
				synchronized (this) {
					// Wait until the piece is locked in place, or it's been
					// speedFactor milliseconds
					this.wait((long) speedFactor);
					// Piece has hit a block
					if (!movePiece(currMovingPiece, 0, 1)) {
						if (!nosleep) {
							timer = new Timer();
							timer.schedule(new TimerThread(this), 1000);
							movingPiece = true;
							// Wait till the user stops moving the piece around
							// for 1 sec
							this.wait();

							// Done moving the piece
							movingPiece = false;
						}
						nosleep = false;
						if (!movePiece(currMovingPiece, 0, 1))
							currMovingPiece = null;
					}
					repaint();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	public void paintComponent(Graphics g) {
		Dimension gridSize = grid.getGridSize();

		// Draw the info panel
		g.setColor(infoPanelColor);
		g.fillRect(gridSize.width * TetrisGrid.getBoxSize(), 0, gridSize.width
				* TetrisGrid.getBoxSize() + INFO_PANEL_WIDTH, gridSize.height
				* TetrisGrid.getBoxSize());
		// Draw the next piece
		int[][] constraint = nextPiece.getConstraint();
		for (int x = 0; x < constraint.length; x++) {
			for (int y = 0; y < constraint[x].length; y++) {
				if (constraint[x][y] == -1) {
					g.setColor(infoPanelColor);
				} else {
					g.setColor(nextPiece.getColor());
				}
				g.fillRect((INFO_PANEL_WIDTH / 2)
						+ ((x + gridSize.width) * TetrisGrid.getBoxSize()) + 1,
						y * TetrisGrid.getBoxSize() + 1, TetrisGrid
								.getBoxSize() - 1, TetrisGrid.getBoxSize() - 1);
			}
		}
		g.setColor(Color.BLACK);
		Font infoFont = new Font(Font.SANS_SERIF, Font.BOLD, 15);
		g.setFont(infoFont);
		g.drawString("Score:", (gridSize.width * TetrisGrid.getBoxSize())
				+ (INFO_PANEL_WIDTH / 2), (gridSize.height / 3)
				* TetrisGrid.getBoxSize());
		g.drawString(score + "", (gridSize.width * TetrisGrid.getBoxSize())
				+ (INFO_PANEL_WIDTH / 2), (gridSize.height / 3)
				* TetrisGrid.getBoxSize() + 20);
		g.drawString("Level:", (gridSize.width * TetrisGrid.getBoxSize())
				+ (INFO_PANEL_WIDTH / 2), (gridSize.height / 2)
				* TetrisGrid.getBoxSize());
		g.drawString(level + "", (gridSize.width * TetrisGrid.getBoxSize())
				+ (INFO_PANEL_WIDTH / 2), (gridSize.height / 2)
				* TetrisGrid.getBoxSize() + 20);

		// Redraw all the pieces
		for (int x = 0; x < gridSize.width; x++) {
			for (int y = 0; y < gridSize.height; y++) {
				// Fill colors
				Color c = grid.colorAt(x, y);
				g.setColor(c);
				g.fillRect(x * TetrisGrid.getBoxSize(), y
						* TetrisGrid.getBoxSize(), TetrisGrid.getBoxSize(),
						TetrisGrid.getBoxSize());
			}
		}

		if (currMovingPiece != null) {
			// Draw a preview at the bottom to show where the piece will fall
			Point dropLocation = peekDrop(currMovingPiece);
			Color pieceColor = currMovingPiece.getType().getColor();
			int[][] currConstraint = currMovingPiece.getCurrentConstraint();

			g.setColor(pieceColor);
			for (int x = 0; x < currConstraint.length; x++) {
				for (int y = 0; y < currConstraint[x].length; y++) {
					if (currConstraint[x][y] != -1) {
						// The constants are to make the squares slightly
						// smaller
						// so they don't interfere w/ grid lines
						g.drawRect((x + dropLocation.x)
								* TetrisGrid.getBoxSize() + 1,
								(y + dropLocation.y) * TetrisGrid.getBoxSize()
										+ 1, TetrisGrid.getBoxSize() - 2,
								TetrisGrid.getBoxSize() - 2);
					}
				}
			}
		}

		for (int x = 0; x < gridSize.width; x++) {
			// Draw the vertical grid lines
			g.setColor(Color.BLACK);
			g.drawLine(x * TetrisGrid.getBoxSize(), 0, x
					* TetrisGrid.getBoxSize(), gridSize.height
					* TetrisGrid.getBoxSize());
		}
		for (int y = 0; y < gridSize.height; y++) {
			// Draw the horizontal grid lines
			g.setColor(Color.BLACK);
			g.drawLine(0, y * TetrisGrid.getBoxSize(), gridSize.width
					* TetrisGrid.getBoxSize(), y * TetrisGrid.getBoxSize());
		}

		if (!playable) {
			Font f = new Font("Tahoma", Font.BOLD, 20);
			g.setFont(f);
			g.setColor(Color.WHITE);
			g.drawString("You LOSE!", 0, getHeight() / 2);
			g.drawString("Press Enter to play again!", 0, getHeight() / 4);
			return;
		}
		if (paused) {
			Font f = new Font("Tahoma", Font.BOLD, 20);
			g.setFont(f);
			g.setColor(Color.WHITE);
			g.drawString("Game is paused", 0, getHeight() / 2);
			g.drawString("Press p to resume", 0,
					(int) (getHeight() * (3.0 / 4.0)));
		}
		if (won) {
			Font f = new Font("Tahoma", Font.BOLD, 20);
			g.setFont(f);
			g.setColor(Color.WHITE);
			g.drawString("YOU WIN OMG", 0, getHeight() / 2);
			g.drawString("Press Enter to play again!", 0,
					(int) (getHeight() * (3.0 / 4.0)));
		}
		// Moves the piece down again
		if (dropping) {
			if (!movePiece(currMovingPiece, 0, 1)) {
				dropping = false;
				nosleep = true;
				currMovingPiece = null;
				synchronized (this) {
					this.notify(); // Notify the thread it can continue
				}
			}
		}
	}

	public void endGame() {
		writeScores();
		displayScores();
		playable = false;
		repaint();
	}

	private void winGame() {
		writeScores();
		displayScores();
		won = true;
		repaint();
	}

	private void writeScores() {
		BufferedWriter bw = null;
		try {
			String name = "";
			name = JOptionPane.showInputDialog("Enter your name to record "
					+ "your score!", "");
			if (name == null) {
				JOptionPane.showMessageDialog(this, "No scores recorded!",
						"Error", JOptionPane.ERROR_MESSAGE);
				return;
			}
			while (name.length() == 0) {
				name = JOptionPane.showInputDialog("Enter your name!", "");
				if (name == null) {
					JOptionPane.showMessageDialog(this, "No scores recorded!",
							"Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
			}

			if (!scoreFile.exists()) {
				if (!scoreFile.createNewFile()) {
					JOptionPane.showMessageDialog(this, "No scores recorded!",
							"Write error", JOptionPane.ERROR_MESSAGE);
					return;
				}
			}
			bw = new BufferedWriter(new FileWriter(SCORE_FILE, true));
			bw.write(name + ":" + score);
			bw.newLine();
			bw.flush();
		} catch (IOException e) {
			JOptionPane.showMessageDialog(this, e.getMessage(), "Write error",
					JOptionPane.ERROR_MESSAGE);
		} finally {
			if (bw != null) {
				try {
					bw.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}

	private void displayScores() {
		scores.populateList();
		scores.displayScores(true);
		sf.setVisible(true);
		scores.invalidate();
	}

	public void keyTyped(KeyEvent e) {
	}

	public void keyPressed(KeyEvent e) {
		if (paused && e.getKeyCode() != KeyEvent.VK_P) {
			return;
		}
		int dx = 0, dy = 0;
		switch (e.getKeyCode()) {
		case KeyEvent.VK_SPACE:
			dropping = true;
			repaint();
			return;
		case KeyEvent.VK_P:
			if (!paused) {
				paused = true;
				repaint();
			} else {
				paused = false;
				new Thread(this).start();
			}
			break;
		case KeyEvent.VK_ENTER:
			if (!playable || won) {
				grid.reset();
				playable = true;
				// Don't show the scores
				scores.displayScores(false);
				sf.setVisible(false);
				sf.invalidate();
				new Thread(this).start();
				score = 0;
				level = 1;
				speedFactor = 1;
				barsDestroyed = 0;
				if (currMovingPiece != null) {
					grid.removePiece(currMovingPiece);
					currMovingPiece = null;
				}
			}
			break;
		case KeyEvent.VK_LEFT:
			dx = -1;
			break;
		case KeyEvent.VK_RIGHT:
			dx = 1;
			break;
		case KeyEvent.VK_UP:
			grid.removePiece(currMovingPiece);
			// Check to see if the piece can be rotated w/o being moved
			currMovingPiece.rotate();
			if (!grid.isValid(currMovingPiece)) {
				// Rotation didn't work
				currMovingPiece.rotateReverse();

				Rectangle container = currMovingPiece.getContainer();
				Dimension gridSize = grid.getGridSize();
				// if the piece is against the right wall...
				// (add height to x pos because the piece, when rotated, will be
				// container.height wide)
				if (container.x + container.height > gridSize.width) {
					// Move the piece left
					currMovingPiece.translate(gridSize.width
							- (container.x + container.height), 0);
					currMovingPiece.rotate();
					// Should be rotated correctly... can't think of a reason
					// why it
					// wouldn't be, but just in case
					assert (grid.isValid(currMovingPiece));
				}
				// If the piece is against the bottom
				if (container.y + container.width > gridSize.height) {
					// Move the piece up
					currMovingPiece.translate(0, gridSize.height
							- (container.y + container.width));
					currMovingPiece.rotate();
					// Should be rotated correctly... can't think of a reason
					// why it
					// wouldn't be, but just in case
					assert (grid.isValid(currMovingPiece));
				}
			}
			grid.addPiece(currMovingPiece);
			repaint();
			break;
		case KeyEvent.VK_DOWN:
			dy = 1;
			break;
		}
		// Reschedule the timer if the user moved the piece
		if (movingPiece) {
			timer.cancel();
			timer = new Timer();
			timer.schedule(new TimerThread(this), 1000);
		}
		movePiece(currMovingPiece, dx, dy);
	}

	public void keyReleased(KeyEvent e) {
	}

	// Returns the location where the piece would be dropped
	private Point peekDrop(Piece toMove) {
		int dy = 0; // How much the piece moved
		grid.removePiece(toMove);
		while (true) {
			toMove.translate(0, 1);
			dy++;
			if (!grid.isValid(toMove)) {
				toMove.translate(0, -1);
				dy--;
				// Piece should be in it's final location...
				Point dropPoint = toMove.getContainer().getLocation();
				// Now that were know where it's going to fall, move it back
				// since it's not supposed to be here yet
				toMove.translate(0, -dy);
				grid.addPiece(toMove);
				return dropPoint;
			}
		}
	}

	// Produces a sequence that goes: 0,-1,1,-2,2,-3,3,etc...
	public int displacement(int x) {
		if (x == 0)
			return -1;
		else if (x < 0)
			return -x;
		else
			return -x - 1;
	}

	// When given a number in the above sequence, produces the previous number
	// ie -1 = 0, 1 = -1, -2 = 1
	public int displacementOpposite(int x) {
		if (x == -1)
			return 0;
		else if (x < 0)
			return -x - 1;
		else
			return -x;
	}

	public boolean movePiece(Piece toMove, int dx, int dy) {
		if (toMove == null) {
			return false;
		}
		if (dx == 0 && dy == 0) {
			return true;
		}
		boolean valid = true;

		// Remove the piece before checking so that it doesn't clash with itself
		grid.removePiece(toMove);
		toMove.translate(dx, dy);
		// Check to see if it's valid...
		if (!grid.isValid(toMove)) {
			// undo move if invalid
			toMove.translate(-dx, -dy);
			valid = false;
		}
		grid.addPiece(toMove);
		repaint();
		return valid;
	}

	public class TimerThread extends TimerTask {
		TetrisPanel tpHandle;

		public TimerThread(TetrisPanel tp) {
			tpHandle = tp;
		}

		@Override
		public void run() {
			synchronized (tpHandle) {
				tpHandle.notify();
			}
		}
	}
}

/*
 * Old weird rotation thing boolean rotated = false;
 * currMovingPiece.rotateReverse(); int[][] constraint =
 * currMovingPiece.getCurrentConstraint();
 * 
 * System.out.println("location before: x=" + currMovingPiece.getContainer().x +
 * ", y=" + currMovingPiece.getContainer().y); // Piece can either move up to (+
 * or -) x, or up to (+ or -) y, // but not both for (int x = 0; x <
 * constraint.length; x=displacement(x)) { currMovingPiece.rotate(); if
 * (!grid.isValid(currMovingPiece)) { // rotate back
 * currMovingPiece.rotateReverse();
 * 
 * // Undo the old move currMovingPiece.translate(displacementOpposite(x), 0);
 * // Try to move the piece then rotate
 * currMovingPiece.translate(displacement(x), 0); } else { rotated = true;
 * break; } } if (rotated) {
 * System.out.println("location after x translation: x=" +
 * currMovingPiece.getContainer().x + ", y=" +
 * currMovingPiece.getContainer().y); break; }
 * System.out.println("constraint size: " + constraint.length);
 * System.out.println("location after x translation: x=" +
 * currMovingPiece.getContainer().x + ", y=" +
 * currMovingPiece.getContainer().y); for (int y = 0; y < constraint[0].length;
 * y=displacement(y)) { currMovingPiece.rotate(); if
 * (!grid.isValid(currMovingPiece)) { // rotate back
 * currMovingPiece.rotateReverse();
 * 
 * // Undo the old move currMovingPiece.translate(0, displacementOpposite(y));
 * // Try to move the piece then rotate currMovingPiece.translate(0,
 * displacement(y)); } else { break; } } if (!rotated) {
 * currMovingPiece.translate(0, constraint[0].length - 1); System.out
 * .println("location after y translation: x=" +
 * currMovingPiece.getContainer().x + ", y=" +
 * currMovingPiece.getContainer().y); }
 */
